#!/bin/bash

#~ set -xv

cache_dir="/var/cache/pacmenu"
config="$HOME/.config/pacmenu/config"
update=0
menu=0
update_function="update1"
# the default update function, checking for ALL $PATH, pretty-formatting
# for longest package name, is update1().
# update2() is a different way to do this, and not yet functional.
# https://bbs.archlinux.org/viewtopic.php?pid=1661072#p1661072
# disadvantages: only checks for /usr/bin, static pretty-formatting
# (tab at 37 chars, that's the longest package name on my system)
# advantages: much more concise, presumably faster.

version="0.1.1"

lines=22
launch_cmd="$(which dmenu)"
[[ "x$launch_cmd" == "x" ]] && usage "dmenu not found in \$PATH."
launch_cmd="$launch_cmd -l $lines -p "

expac_cmd="$(which expac)"
[[ "x$expac_cmd" == "x" ]] && { echo "expac not found in \$PATH. Install expac, or edit this script."; exit 1; }

terminal_cmd="$XTERMINAL"
# if the env-var XTERMINAL is defined, use that.

######################### FUNCTIONS #############################

usage() {
	[[ "x$1" != "x" ]] && echo -e "$1"
	echo "
Usage: pacmenu [-m] [-d dir] [-l launcher] [-c config] [-t term] [-h] [-v]

lists installed packages containing executables in \$PATH, with description.
Choosing a package presents a list of these executables, to choose & execute.

  -m Show the menu. This is the default if no options are given.

  -u Update cache. Won't show the menu unless specified with -m.

  -d Cache directory. Applies to -m and -u options. In it, the files packages
     and paths are created. Defaults to $cache_dir.

  -l Launcher command. Defaults to $launch_cmd.

  -c Read config file. Defaults to ~/.config/pacmenu/config. Sourced by bash.
     Command line options override config options.

  -t Terminal emulator for final command. Not given, the script uses \$TERMINAL,
     if defined. To force direct execution w/o terminal, supply an empty string."
	exit 1
}
update1() {
	mkdir -p "$cache_dir"
	# directory not writeable? abort!
	[[ ! -w "$cache_dir" ]] && usage "Directory $cache_dir not writeable!"

	[[ -e "$pathsfile" ]] && [[ ! -w "$pathsfile" ]] && usage "File $pathsfile cannot be overwritten!"
	paths="${PATH:1}"
	paths="${paths//:\//|}"
	echo "$paths" > "$pathsfile.tmp"

	[[ -e "$pkgfile" ]] && [[ ! -w "$pkgfile" ]] && usage "File $pkgfile cannot be overwritten!"

	#~ array_bin=($(cd /var/lib/pacman/local/ && /usr/bin/grep -lsE "$paths" */files))
	# list of packages that install files in /usr/bin (assuming that directory names
	# in /var/lib/pacman/local always start with package names)
	# each array element still contains trailing cruft, but
	# it starts with the packagename, which is what we need

	oldifs="$IFS"
	IFS=$'\n'
	array=($($expac_cmd -Q '%n\t%d\t%F' | awk -F"\t" -v paths="$paths" '$0 ~ paths { printf "%s %s\n", $1, $2; }'))
	# an array of packages' names & descriptions, that have executables in $PATH
	IFS="$oldifs"

	#~ count=0
	#~ countarraycount=0
	# the countarray will hold indices of packages that install files in /usr/bin

	longest=0 # used for pretty-formatting (dmenu can't handle tabs?)
	for (( count=0; count < ${#array[*]}; count++ )); do
		length="${array[count]%% *}"
		length="${#length}"
		(( length > longest )) && longest=$length
	done
	# 1st loop: loop through list of packages that install files in /usr/bin
	#~ while [[ "${array_bin[countarraycount]}" != "" ]]
	#~ do
		#~ if [[ "${array_bin[countarraycount]%%/*}" =~ "${array[count]%% *}" ]]
		#~ # if the packagename equals one from the array of ALL packages...
		#~ then
			#~ countarray[((countarraycount++))]=$count
			#~ # ... then add the index to the countarray
			#~ # while we're at it, get the length of longest package for later pretty-formatting
			#~ length="${array[count]%% *}"
			#~ length="${#length}"
			#~ (( length > longest )) && longest=$length
		#~ fi
		#~ ((count++))
	#~ done
	#~ unset array_bin

	# 2nd loop: pretty-formatting and write to $pkgfile
	for (( count=0; count < ${#array[*]}; count++ ))
	do
		pkgname="${array[count]%% *}"
		description="${array[count]#* }"
		length=$((longest - ${#pkgname}))
		for (( j=0 ; j<length ; j++ ))
		do
			pkgname="$pkgname "
		done
		echo "$pkgname  $description"
	done > "$pkgfile"
	mv -f "$pathsfile.tmp" "$pathsfile"
}

# doesn't work, but worth investigating
# see https://bbs.archlinux.org/viewtopic.php?pid=1661072#p1661072
update2() {
	paths="${PATH:1}"
	paths="${paths//:\//|}"
	echo "$paths" > "$pathsfile"
	paths="${paths//\//\\/}"
	echo "$paths"
	$expac_cmd -Q '%n\t%d\t%F' | awk -F"\t" '/ usr\/bin\/[^$]/ { printf "%-37s %s\n", $1, $2; }' > "$pkgfile"
	#~ $expac_cmd -Q '%n\t%d\t%F' | awk -F"\t" '/ "$paths"[^$]/ { printf "%-37s %s\n", $1, $2; }' > "$pkgfile"
}

menu() {
	[[ ! -r "$pkgfile" ]] && usage "File $pkgfile is not readable!"

	choice="$($launch_cmd -i <"$pkgfile")"
	choice="${choice%% *}"

	if [[ "x$choice" != "x" ]]
		then
		choice=($($expac_cmd -l'\n' '%F' "$choice"))
		# an array of all files that are installed by the chosen package
		# (the variable choice jumps from being a normal variable, then an array,
		# and back to a normal var again. it seems bash can take it.)

		paths="$(<"$pathsfile")"
		# read back paths from file (so that we get the $PATH of the user that 
		# created the menu file)

		# check all files from the array whether they're in $paths AND executable.
		# the next step (solved with a case/esac) is to filter out subdirectories
		# (yes, some packages have executables & subdirectories mixed all together)
		# and empty results, then re-attach the leading /
		choice="$(for (( x=0 ; x< ${#choice[*]} ; x++ ))
		do
			for i in ${paths//|/ }
			do
				if [[ "${choice[x]}" == "$i"* ]] && [ -x "/${choice[x]}" ]
				then
					case "${choice[x]/$i\//}" in
						*\/*|'') true
						;;
						*) echo /${choice[x]}
						;;
					esac
				fi
			done
		done | $launch_cmd)"
	echo "$choice"
		# if the user chose something, execute it either in terminal or directly.
		if [[ "x$choice" != "x" ]]
			then
				if [[ "x$terminal_cmd" != "x" ]] 
				then
					if [[ "$choice" == *\; ]]
					then
						choice="${choice::-1}"
						if [[ "$choice" == *\; ]]
						then
							choice="$choice$SHELL"
						fi
						exec $terminal_cmd -e sh -c "$choice" & disown
					else
						exec "$choice" & disown
					fi
				else 
					exec "$choice" & disown
				fi
		fi
	fi
}

######################### MAIN ######################################

[[ -f "$config" ]] && { . "$config" || usage "Problem sourcing $config"; }

while getopts "mud:l:c:t:hv" opt; do
case "$opt" in
	m)	menu=1
	;;
	u)	update=1
			#~ case "$OPTARG" in
				#~ 1|2) update_function="update$OPTARG"
				     #~ ;;
				#~ *) usage "Please choose Update method 1 or 2"
				#~ ;;
			#~ esac
	;;
	d)	cache_dir="$OPTARG"
	;;
	l)	launch_cmd="$OPTARG"
	;;
	c)	config="$OPTARG"
	[[ ! -r "$config" ]] && usage "Config file $config is not readable!"
	;;
	t)	terminal_cmd="$OPTARG"
	;;
	v)	echo "Version: $version"; exit 0
	;;
	*)	usage
	;;
esac
done

pkgfile="$cache_dir/packages"
pathsfile="$cache_dir/paths"

# default action is to show the menu.
[[ "$update" == "0" ]] && menu=1

if [[ "$menu" == "0" ]]
then
	[[ "$update" == "1" ]] && "$update_function" &
else
	[[ "$update" == "1" ]] && "$update_function"
	[[ "$menu" == "1" ]] && menu
fi

exit 0
